背景
前面开发的多租户多数据库方案功能完备但复杂度高——需要同时维护 TypeORM、Mongoose、Prisma 三个 ORM 模块。对于体量较小的项目,这种架构过于沉重。
本节的目标是:通过环境变量配置,让同一套代码既能运行在单数据库模式(只加载一个 ORM),也能运行在多租户多数据库模式(按需加载多个 ORM)。
环境变量设计
在 .env 中新增三个关键配置项:
# 是否开启多租户模式
TENANT_MODE=true
# 支持的 ORM 类型,逗号分隔
TENANT_DB_TYPE=typeorm,mongoose,prisma
# 默认数据库连接(存放 tenant 与 user 的关联关系)
TENANT_DB_DEFAULT=postgresql://user:pass@localhost:5432/tenant_db
bash
说明:
TENANT_MODE=false时,项目只加载默认 ORM(如 Prisma),对接单一数据库TENANT_DB_TYPE使用英文逗号分隔,支持typeorm、mongoose、prisma三种值TENANT_DB_DEFAULT用于存储租户标识与用户的映射关系
提取公共配置读取工具
由于多处需要读取 .env 配置,抽取公共方法避免重复代码:
// utils/getEnv.ts
import * as dotenv from 'dotenv';
import * as fs from 'fs';
export function getEnv(): Record<string, string | undefined> {
const envFiles = ['.env', `.env.${process.env.NODE_ENV || 'development'}`];
const parsedConfig: Record<string, string | undefined> = {};
envFiles.forEach((path) => {
if (fs.existsSync(path)) {
const config = dotenv.parse(fs.readFileSync(path));
Object.assign(parsedConfig, config);
}
});
return parsedConfig;
}
typescript
DatabaseModule 的条件式加载
读取配置并动态决定 imports
// databases.module.ts
import { toBoolean } from './utils/format';
import { getEnv } from './utils/getEnv';
@Module({})
export class DatabasesModule {
static register(): DynamicModule {
const parsedConfig = getEnv();
const tenantMode = toBoolean(parsedConfig['TENANT_MODE']);
const tenantDbType = parsedConfig['TENANT_DB_TYPE'] || '';
let imports: DynamicModule['imports'];
if (tenantMode) {
// 多租户模式:按配置加载对应的 ORM 模块
imports = tenantDbType
.split(',')
.map((type) => {
switch (type.trim()) {
case 'typeorm':
return TypeOrmCommonModule;
case 'prisma':
return PrismaCommonModule;
case 'mongoose':
return MongooseCommonModule;
default:
return undefined;
}
})
.filter((item): item is DynamicModule => item !== undefined);
} else {
// 单数据库模式:只加载默认 ORM
imports = [PrismaCommonModule];
}
return {
module: DatabasesModule,
imports,
};
}
}
typescript
注意 .map() 后必须加 .filter() 过滤 undefined,否则 NestJS 会报 module index undefined 错误。
单数据库模式的 Prisma 配置
当 TENANT_MODE=false 时,Prisma 需要读取默认的数据库连接字符串。在 PrismaConfigService 中处理:
// prisma-config.service.ts
import { ConfigService } from '@nestjs/config';
@Injectable()
export class PrismaConfigService extends PrismaClient {
constructor(private configService: ConfigService) {
super({
datasources: {
db: {
url: configService.get<string>('DATABASE_URL'),
},
},
});
}
}
typescript
注意 url 字段需要包裹在 datasources.db 对象中,直接返回字符串会导致连接失败。
UserModule 的条件式 Providers
UserModule 也需要根据配置动态提供 Repository 实例。核心逻辑与 DatabaseModule 一致:
// user.module.ts
const tenantMode = toBoolean(parsedConfig['TENANT_MODE']);
const tenantDbType = parsedConfig['TENANT_DB_TYPE'] || '';
let imports: any[] = [];
let providers: any[] = [];
if (tenantMode) {
// 根据配置加载对应的模块和 Repository
if (tenantDbType.includes('typeorm')) {
imports.push(TypeOrmModule.forFeature([User]));
providers.push(UserTypeOrmRepository);
}
if (tenantDbType.includes('prisma')) {
imports.push(PrismaModule.forFeature([User]));
providers.push(UserPrismaRepository);
}
if (tenantDbType.includes('mongoose')) {
imports.push(MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]));
providers.push(UserMongooseRepository);
}
} else {
// 单数据库模式:保留默认 Repository
providers.push(UserPrismaRepository);
}
@Module({
imports: [
...imports,
// 其他固定 imports
],
providers: [
...providers,
UserRepository, // 公共 Repository 必须始终提供
],
})
export class UserModule {}
typescript
Optional 装饰器处理可选依赖
在公共 UserRepository 中,不同的 ORM Repository 是可选的。使用 @Optional() 装饰器标记:
@Injectable()
export class UserRepository {
constructor(
@Optional() @Inject('TYPEORM_REPO') private typeOrmRepo: UserTypeOrmRepository,
@Optional() @Inject('PRISMA_REPO') private prismaRepo: UserPrismaRepository,
@Optional() @Inject('MONGOOSE_REPO') private mongooseRepo: UserMongooseRepository,
) {}
getRepository(tenantId?: string) {
if (tenantId === 'mysql1') return this.typeOrmRepo;
// ... 其他判断
return this.prismaRepo; // 默认返回 Prisma Repository
}
}
typescript
常见问题与排错
| 问题 | 原因 | 解决方案 |
|---|---|---|
Module index undefined | map 过滤不彻底,返回了 undefined | 在 .map() 后加 .filter(item => item !== undefined) |
Cannot find provider | 未加载的 ORM 的 Repository 未被提供 | 使用 @Optional() 装饰器标记可选依赖 |
| Prisma 连接失败 | datasources.db.url 结构错误 | 确保 Prisma 配置包裹在 { datasources: { db: { url: ... } } } 结构中 |
DATABASE_URL 格式错误 | 连接字符串前缀写错 | PostgreSQL 应为 postgresql:// 而非 postgres:// |
改造要点总结
- DatabaseModule:通过环境变量动态加载 ORM 模块,非租户模式保留默认 imports
- UserModule:与 DatabaseModule 逻辑类似,非租户模式保留默认 Repository
- Controller/Repository:使用
@Optional()装饰器处理可选的 ORM 实例 - PrismaConfigService:注意
datasources的嵌套结构 .env文件:DATABASE_URL的协议前缀需正确填写
↑